package cn.edu.dgut.css.sai.demo.remoteloadjar.local;

import org.springframework.boot.context.config.ConfigFileApplicationListener;
import org.springframework.boot.loader.JarLauncher;
import org.springframework.boot.loader.LaunchedURLClassLoader;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.util.Assert;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.List;
import java.util.Objects;
import java.util.Properties;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * <h2>Spring-Boot应用程序-远程加载jar文件的设计与实现：</h2>
 * 我们知道，Java利用ClassLoader将类载入内存，并且在同一应用中，可以有很多个ClassLoader，通过委派机制，把装载的任务传递给上级的装载器的，依次类推，直到启动类装载器（没有上级类装载器）。如果启动类装载器能够装载这个类，那么它会首先装载。如果不能，则往下传递。当父类为null时，JVM内置的类(称为:bootstrap class loader)就会充当父类。想想眼下的越来越多用XML文件做配置文件或者是描述符、部署符。其实这些通过XML文档描述的配置信息最终都要变成Java类，基实都是通过ClassLoader来完成的。<br/>
 * <br/>
 * Java程序开始执行，遇到的第一个classloader是bootstrap classloader，这个classloader是用c++语言编写，通过他来完成加载java中的核心类;
 * 第二个classloader是extension classloader，加载的是jre/lib目录中的ext目录中的jar包。
 * 然后第三个是system classloader，也被称为应用加载器，主要负责完成加载-classpath 或者系统中的全局变量ClassPath中的类，
 * System.out.println(System.getProperty(“java.class.path”));可以获得classpath的配置，也就是system classloader 加载的类;
 * 第四个class loader可能是用户自定义的加载器，来自定义加载类，
 * 通常一个类的加载过程是这样的通过当前的类加载器的父加载器尝试查找，如果没有再找其父加载器尝试加载，直到最终的bootstrap classloader为止，如果还没有找到，那么就开始从上往下加载类。这样做的目的是防止自定义的类来覆盖系统中的类。
 * <p></p>
 * 参考文献：
 * <ol>
 *     <li>https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html</li>
 *     <li>https://blog.csdn.net/wawmg/article/details/17961815</li>
 *     <li>https://www.cnblogs.com/franson-2016/p/6145822.html</li>
 *     <li>https://www.cnblogs.com/xwdreamer/archive/2011/12/05/2296918.html</li>
 * </ol>
 * <h2>{@link URLClassLoader}</h2>
 * URLClassLoader是ClassLoader的子类，它用于从指向 JAR 文件和目录的 URL 的搜索路径加载类和资源(支持file、http协议，可远程加载Jar资源文件)，
 * 也就是说，通过URLClassLoader就可以加载指定jar(如：外部、远程Jar文件)中的class到内存中。
 * <p></p>
 * <h2>Spring boot应用的定制类加载器{@link LaunchedURLClassLoader}</h2>
 * 由 spring-boot-maven-plugin 的maven插件重新打包的可执行jar是有固定的结构的，详情可以看：https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-executable-jar-format.html#executable-jar <br/>
 * Spring-Boot会把{@link LaunchedURLClassLoader}配置为线程的上下文ClassLoader{@code Thread.currentThread().setContextClassLoader()}。
 * Spring-Boot应用程序时，默认会使用线程的上下文ClassLoader{@code Thread.currentThread().getContextClassLoader()}加载自动配置类、扫描组件。因此，它可以加载 Fat Jar结构中的配置类与组件。<br/>
 * 要注意的是，{@link ConfigFileApplicationListener}默认使用的是{@code getClass().getClassLoader()}而不是{@code Thread.currentThread().getContextClassLoader()}。
 * <p></p>
 * <h2>实现原理</h2>
 * 经过上面的分析后，想实现Spring-Boot应用程序加载远程Jar文件中的类或资源文件，可以实例化一个{@link URLClassLoader},并配置它的ucp{@link URLClassPath},这样它就可以从远程Jar文件中加载类；<br/>
 * 同时根据ClassLoader双亲委托机制，把{@code Thread.currentThread().getContextClassLoader()}配置为{@link URLClassLoader}的父ClassLoader，这样它既可以读取Fat Jar目录结构中的类和资源文件，又可以远程加载类和资源文件。<br/>
 * 最后，把这个配置后的{@link URLClassLoader} 重新赋值给 线程上下文ClassLoader，这样Spring-Boot应用程序在启动过程中，就会自动使用这个定制的{@link URLClassLoader}加载类和资源文件。也就是说，现在我们Spring-Boot应用程序可以加载远程Jar中的类和资源文件了。Done!
 *
 * <p></p>
 *
 * @author Sai
 * <p></p>
 * {@link ClassLoader}
 * <br/>
 * {@link ClassLoaders.AppClassLoader} 系统ClassLoader，<br/>
 * spring-boot的启动类{@link JarLauncher}是由它加载的。JarLauncher会用反射调用 主类的main方法，{@link }
 * <br/>
 * {@link LaunchedURLClassLoader} Spring-boot框架自定义的ClassLoader，用于加载Fat Jar文件结构。
 * <br/>
 * {@link URLClassLoader}
 * <br/>
 * {@link Thread#currentThread()}
 * <br/>
 * {@link Thread#setContextClassLoader(ClassLoader)}
 * <br/>
 * {@link JarLauncher#main(String[])} 断点调试可以用这个方法开始。观察{@code Thread.currentThread().getContextClassLoader()}和{@code getClass().getClassLoader()}的值的变化。
 * @since 2020/6/8.
 */
@SuppressWarnings("JavadocReference")
public abstract class RemoteClassLoader {

    public static void init() {
        // 远程jar文件的地址在remote.properties文件中。
        ClassPathResource cpr = new ClassPathResource("remote.properties");
        Properties remoteProperties = null;
        try {
            remoteProperties = PropertiesLoaderUtils.loadProperties(cpr);
        } catch (IOException e) {
            e.printStackTrace();
        }
        Assert.notNull(remoteProperties, "读取remote.properties文件失败!");
        String[] jarUrls = remoteProperties.getProperty("sai.remote.jarUrls").split(",");
        List<URL> urls = Stream.of(jarUrls).map(url -> {
            try {
                return URI.create(url).toURL();
            } catch (MalformedURLException e) {
                e.printStackTrace();
                return null;
            }
        }).filter(Objects::nonNull).collect(Collectors.toList());
        // urls.toArray(new URL[0]) List转换为数组，是新数组；如果数组定义的长度少于List的实际长度时，以List的实际长度初始化。
        URLClassLoader urlClassLoader = new URLClassLoader(urls.toArray(URL[]::new), Thread.currentThread().getContextClassLoader());
        Thread.currentThread().setContextClassLoader(urlClassLoader);
    }
}
